﻿// -----------------------------------------------------------------------
// <copyright file="SubscriptionHandler.cs" company="Zen Design Corp">
// TODO: Update copyright text.
// </copyright>
// -----------------------------------------------------------------------

namespace Zen.BlackBerry.Subscription.Web
{
	using System;
	using System.Security.Principal;
	using System.Text;
	using System.Threading.Tasks;
	using System.Web;
	using Microsoft.Practices.ServiceLocation;
	using Zen.BlackBerry.Common;
	using Zen.BlackBerry.Subscription.Auth;
	using Microsoft.Practices.Unity;

	/// <summary>
	/// Abstract HTTP handler from which all subscription operation handlers
	/// are derived.
	/// </summary>
	/// <remarks>
	/// Subscription operation handlers are stateless and reusable.
	/// </remarks>
	public abstract class SubscriptionHandler : IHttpAsyncHandler
	{
		/// <summary>
		/// Initializes a new instance of the <see cref="SubscriptionHandler"/> class.
		/// </summary>
		public SubscriptionHandler()
		{
			// Resolve any instance dependencies.
			IUnityContainer container = 
				ServiceLocator.Current.GetInstance<IUnityContainer>();
			container.BuildUp(this);
		}

		/// <summary>
		/// Gets a value indicating whether another request can use the
		/// <see cref="T:System.Web.IHttpHandler"/> instance.
		/// </summary>
		/// <value></value>
		/// <returns>
		/// true if the <see cref="T:System.Web.IHttpHandler"/> instance is
		/// reusable; otherwise, false.
		/// </returns>
		public bool IsReusable
		{
			get
			{
				return true;
			}
		}

		/// <summary>
		/// Gets the type of the subscription operation handled.
		/// </summary>
		/// <value>The type of the subscription.</value>
		public abstract SubscriptionType SubscriptionType
		{
			get;
		}

		/// <summary>
		/// Gets or sets the content provider authentication service.
		/// </summary>
		/// <value>The content provider authentication service.</value>
		/// <remarks>
		/// This is set by the IoC container.
		/// </remarks>
		[Dependency]
		public IContentProviderAuthenticationService AuthenticationService
		{
			get;
			set;
		}

		/// <summary>
		/// Gets or sets the subscription service.
		/// </summary>
		/// <value>The subscription service.</value>
		/// <remarks>
		/// This is set by the IoC container.
		/// </remarks>
		[Dependency]
		public ISubscriptionService SubscriptionService
		{
			get;
			set;
		}

		/// <summary>
		/// Enables processing of HTTP Web requests by a custom HttpHandler
		/// that implements the <see cref="T:System.Web.IHttpHandler"/>
		/// interface.
		/// </summary>
		/// <param name="context">
		/// An <see cref="T:System.Web.HttpContext"/> object that provides
		/// references to the intrinsic server objects (for example, Request,
		/// Response, Session, and Server) used to service HTTP requests.
		/// </param>
		public void ProcessRequest(HttpContext context)
		{
			try
			{
				// Authenticate caller
				string subscriberId = AuthenticateSubscriber(context, SubscriptionType);
				if (string.IsNullOrEmpty(subscriberId))
				{
					return;
				}

				BaseRequest request = GetRequest(context);
				request.SubscriberId = subscriberId;
				PerformSubscriptionOperation(context, request).Wait();
			}
			catch
			{
				SetResponse(context.Response, SubscriptionStatusCode.SystemError);
			}
		}

		/// <summary>
		/// Initiates an asynchronous call to the HTTP handler.
		/// </summary>
		/// <param name="context">An <see cref="T:System.Web.HttpContext"/>
		/// object that provides references to intrinsic server objects (for
		/// example, Request, Response, Session, and Server) used to service
		/// HTTP requests.
		/// </param>
		/// <param name="cb">
		/// The <see cref="T:System.AsyncCallback"/> to call when the
		/// asynchronous method call is complete. If <paramref name="cb"/> is
		/// null, the delegate is not called.
		/// </param>
		/// <param name="extraData">
		/// Any extra data needed to process the request.
		/// </param>
		/// <returns>
		/// An <see cref="T:System.IAsyncResult"/> that contains information
		/// about the status of the process.
		/// </returns>
		public IAsyncResult BeginProcessRequest(
			HttpContext context, AsyncCallback cb, object extraData)
		{
			TaskCompletionSource<bool> tcs = new TaskCompletionSource<bool>(extraData);

			// Authenticate caller
			string subscriberId = AuthenticateSubscriber(context, SubscriptionType);
			if (string.IsNullOrEmpty(subscriberId))
			{
				tcs.SetResult(true);
				return tcs.Task;
			}

			// Get the request and setup the subscriber
			BaseRequest request = GetRequest(context);
			request.SubscriberId = subscriberId;

			// Perform the subscription operation asynchronously
			// NOTE: We do not spin off a new thread here - no need!
			PerformSubscriptionOperation(context, request)
				.ContinueWith((result) => tcs.SetFromTask(result));
			return tcs.Task;
		}

		/// <summary>
		/// Provides an asynchronous process End method when the process ends.
		/// </summary>
		/// <param name="result">
		/// An <see cref="T:System.IAsyncResult"/> that contains information
		/// about the status of the process.
		/// </param>
		public void EndProcessRequest(IAsyncResult result)
		{
			TaskCompletionSource<bool> tcs =
				result as TaskCompletionSource<bool>;
			if (tcs == null)
			{
				throw new ArgumentException(
					"IAsyncResult not from this object.", "result");
			}

			// Wait for task to complete
			tcs.Task.Wait();
		}

		/// <summary>
		/// Gets the authentication request object.
		/// </summary>
		/// <param name="request">The request.</param>
		/// <param name="subscriptionType">Type of the subscription.</param>
		/// <returns></returns>
		protected AuthenticationRequest GetAuthenticationRequest(
			HttpRequest request, SubscriptionType subscriptionType)
		{
			AuthenticationRequest result = 
				SubscriptionRequestParser.ParseAuthenticationRequest(request);
			result.SubscriptionType = subscriptionType;
			return result;
		}

		/// <summary>
		/// Authenticates the subscriber.
		/// </summary>
		/// <param name="context">The context.</param>
		/// <param name="subscriptionType">Type of the subscription.</param>
		/// <returns>
		/// The subscriberId resulting from successfully authenticating the
		/// caller; otherwise null if the caller cannot be authenticated.
		/// </returns>
		protected string AuthenticateSubscriber(
			HttpContext context, SubscriptionType subscriptionType)
		{
			string subscriberId = null;

			if (context.Request.IsAuthenticated)
			{
				subscriberId = context.User.Identity.Name;
			}
			else
			{
				try
				{
					AuthenticationRequest auth = 
						GetAuthenticationRequest(context.Request, subscriptionType);
					IPrincipal principal = 
						AuthenticateSubscriber(context.Response, auth);
					if (principal == null)
					{
						throw new InvalidOperationException("Authentication failure.");
					}

					// Setup user on request object and principal name is the subscriber id
					context.User = principal;
					subscriberId = principal.Identity.Name;
				}
				catch
				{
					SetResponse(context.Response, SubscriptionStatusCode.SubscriberAuthenticationFailed);
					return null;
				}
			}

			if (string.IsNullOrEmpty(subscriberId))
			{
				SetResponse(context.Response, SubscriptionStatusCode.InvalidSubscriberId);
			}

			return subscriberId;
		}

		/// <summary>
		/// Authenticates the subscriber.
		/// </summary>
		/// <param name="response">The response.</param>
		/// <param name="request">The request.</param>
		/// <returns>
		/// An <see cref="IPrincipal"/> object that represents the authenticated
		/// caller; otherwise null if the caller cannot be authenticated.
		/// </returns>
		protected virtual IPrincipal AuthenticateSubscriber(
			HttpResponse response, AuthenticationRequest request)
		{
			IPrincipal principal = 
				AuthenticationService.AuthenticateSubscriber(request);
			AddAuthenticationCredentials(response, principal);
			return principal;
		}

		/// <summary>
		/// Adds authentication credentials from the subscriber principal to
		/// the HTTP response in the form of headers.
		/// </summary>
		/// <param name="response">The response.</param>
		/// <param name="principal">The principal.</param>
		protected virtual void AddAuthenticationCredentials(
			HttpResponse response, IPrincipal principal)
		{
			if (principal == null)
			{
				throw new ArgumentNullException("principal");
			}

			response.Headers.Add("name", principal.Identity.Name);
		}

		/// <summary>
		/// Sets the specified status code into the HTTP response.
		/// </summary>
		/// <param name="response">The response.</param>
		/// <param name="statusCode">The status code.</param>
		protected void SetResponse(
			HttpResponse response, SubscriptionStatusCode statusCode)
		{
			try
			{
				response.Headers["Pragma"] = "no-cache";
				response.Headers["Cache-Control"] = "no-cache";

				response.ContentType = "text/plain";
				byte[] bytes = Encoding.UTF8.GetBytes(
					XmlExtensions.GetTextFromEnum<SubscriptionStatusCode>(statusCode));
				response.OutputStream.Write(bytes, 0, bytes.Length);
			}
			catch (Exception e)
			{
				//log.warn("IOException caught while trying to set response: ", e);
				throw e;
			}
		}

		/// <summary>
		/// Gets the subscription operation details parsed out of the HTTP
		/// request.
		/// </summary>
		/// <param name="context">The context.</param>
		/// <returns>
		/// A <see cref="BaseRequest"/> containing the subscription details.
		/// </returns>
		protected abstract BaseRequest GetRequest(HttpContext context);

		/// <summary>
		/// Asynchronously performs the subscription operation.
		/// </summary>
		/// <param name="context">The context.</param>
		/// <param name="request">The request.</param>
		/// <returns></returns>
		protected abstract Task PerformSubscriptionOperation(
			HttpContext context, BaseRequest request);
	}
}
